Typed actions
Declare actions with a fluent builder backed by any Standard Schema validator - Zod, Valibot, ArkType, Effect Schema. The MCP tool schema is derived automatically.
Typed actions
Declare actions with a fluent builder backed by any Standard Schema validator - Zod, Valibot, ArkType, Effect Schema. The MCP tool schema is derived automatically.
Real UI, not a shadow DOM
The agent drives your actual running app. State, auth, feature flags - all intact. Nothing to scrape, nothing to re-implement.
Full MCP capability set
Streaming progress, cancellation, resources (read + subscribe), sampling, and elicitation work out of the box over a single WebSocket.
Framework-agnostic
One-file integrations for vanilla TS, React, Svelte, Vue, Node, and Express. Same builder API everywhere.
import { tesseron } from '@tesseron/web';import { z } from 'zod';
tesseron.app({ id: 'shop', name: 'Acme Shop' });
// 1. A plain action - input, handler, streaming progress, return value.tesseron .action('searchProducts') .describe('Search the product catalog') .input(z.object({ query: z.string().min(1), limit: z.number().default(10) })) .handler(async ({ query, limit }, ctx) => { ctx.progress({ message: 'searching...', percent: 20 }); const items = await store.search(query, { limit }); return { items }; // becomes the MCP tool result the agent sees });
// 2. An action that pauses to ask the user through the agent's UI.tesseron .action('checkout') .describe('Place the pending order') .input(z.object({ cartId: z.string() })) .handler(async ({ cartId }, ctx) => { const ok = await ctx.confirm({ question: `Place order for $${cart.total(cartId)}? This charges your card.`, }); if (!ok) throw new Error('User cancelled'); return await orders.place(cartId); });
// 3. A resource - readable, subscribable app state. No polling needed.tesseron .resource('currentRoute') .describe('URL the user is viewing') .read(() => location.pathname) .subscribe((emit) => { const fn = () => emit(location.pathname); addEventListener('popstate', fn); return () => removeEventListener('popstate', fn); });
// 4. Connect. `connect()` resolves with the claim code - surface it// in your UI so the human can paste it into their agent.const { claimCode } = await tesseron.connect();document.querySelector('#connect-banner')!.textContent = `Paste "${claimCode}" into Claude to connect this tab.`;What the agent sees once connected:
shop__searchProducts and shop__checkout. It can call either, pass typed input, and receive your typed output.tesseron://shop/currentRoute. It can read once, or subscribe and get pushed updates every time the user navigates - no polling, no webhooks.What you didn't have to do:
searchProducts's output, picks a product, calls checkout with it, and pauses on ctx.confirm until the user approves - all orchestrated by the agent loop.That's the whole surface: .action(), .resource(), and .connect(). Everything else is detail.
The other half runs next to the agent. The gateway is @tesseron/mcp - an MCP server that opens the WebSocket port, hands out claim codes, and translates MCP tool calls into actions/invoke frames on your app's socket. You don't write MCP code; the gateway is the MCP server.
You wire it into your agent's MCP config once. Claude Desktop example (claude_desktop_config.json):
{ "mcpServers": { "tesseron": { "command": "npx", "args": ["-y", "@tesseron/mcp"] } }}Claude Code / Cursor / any MCP-capable client: same pattern, their own config file.